Working with one environment is great, working with multiple environment can be better ! The tricks we will see here will help you in using more than one environment at a time.
One use case for such setup can be multi-threaded processing : environments can only be used in one thread at a time. Or it can be useful to expose some functions and set of variables in one particular environment, while having another one exposing different stuff. This could be used for security to avoid corruption in a sensitive part if a script fails mid way...
In this tutorial, we will see :
All these topics requires some thought when designing your pipeline. Luckily, nkScripts tries to make it easy to implement !
One thorny point when using many environment is about communicating data between them. Each having their own memory space, it means that nothing is shared between them. To pass data around, the mechanism we will use is serialization.
Let's put up our scenario. We have some heavy processing we need to do in our scripting environment. However, our scripts are running on the main thread, as their logic update things needed in the rendering loop. The solution will be to run multiple environments, with one running on the main thread while the other one will be on its own processing thread.
This architecture has its chance to work, however we need a way to communicate between both environment to pass input and output where it needs to be. For that, we will use provided serialization and deserialization functions, allowing to translate data into an intermediate state that both environments will be able to interpret.
First, let's define our input in the main environment requiring the processing :
That code defines an object, data, in the environment. It gets 2 members, that we will request our off-thread environment to fill. For that, we first need to get the object and serialize it. C++ will be the common ground where this will happen :
To serialize an object, we need a ScriptObjectReference over it. This is required because serialization can take arbitrary data, which is maybe not expressible in C++. For instance, here, we serialize a complex Lua object along with its members.
Serialization outputs a binary buffer with all the information an environment would require to reconstruct the object. Exactly what we need ! We can now pass the buffer to the other thread, and deserialize it in its environment :
This line does deserializes the buffer into a variable in the environment, returning a reference over it. However, at this point, the variable is not assigned to anything inside the environment. There would be no way to address it. As such, we need a last line to be able to use it :
We have to set another reference in the environment to the variable we created, that we name "data". This behaviour is required because deserializing cannot be written to arbitrary places in the environment. There could be name clashs causing variables to be overwritten, which would be quite annoying.
Now that our variable is ready to be addressed in a script, we can request our environment to execute its processing. Remember, this code is not running on the main thread, which means that it will not block our main thread :
Okay, written like this, the code is not very impressive and could have run in the main thread. However, reaching the final answer, 42, could require more computing power than demonstrated in this simple example. In such cases, this high workload could be placed here.
Data has been processed, we need to make it pass the other way around. You guessed it, we will serialize again, before deserializing it, this time in the main thread's environment. In the thread that just processed everything, we will :
We overwrite the buffer with the new data serialized. Once this is done, we can pass it back to the main thread, deserialize and assign it again within the main environment.
As you noticed, we overwrote the reference we had on our input data, within the environment. This is because deserialization creates new variables we need to assign back.
Let's witness what we now have in our main environment :
As we can witness in the log, the result of our processing is now printed in the main environment ! Whenever data needs to be exchanged, serialization is the way to go. For simple data types, it might be possible to just pass them through the simpler setVar & co methods. However, serialization will ensure the data can be augmented easily if required. Plus, it will handle script specific objects, which is something not possible in C++.
Now that we have the basic tool to deal with environment communication, we need to learn a bit about the user data and how they are handled.
As we know from past tutorials, user data has an owner that should delete the data once it is not needed. It can be C++ or an environment, in which case the garbage collection, if any, will handle it through specified destructor.
Let's take back our example from last part. Our off-thread environment can be altered to also define the nkTutorial::Data structure we specified in past tutorials. This means that our code can do :
Now, when processing the data, our threaded script also augment the object with an allocation of a Data instance. This is all fine, but then, the owner of the memory will be the environment which created it. If we need to keep this output for a longer time in our main thread, but the threaded environment garbage collects the variable, our program will go bonkers !
To avoid that, we need to forward the ownership to our main thread. This way, the main thread would become the owner of given object, and garbage collection in the other thread would have no impact. To achieve this, we need to change one parameter in our serialization function call, which is false by default :
The second argument is about ownership forwarding. If activated, it means that ownership of all user data concerned in the serializing script will be disabled for calling environment. With that, any environment that will deserialize the data will consider itself as owning user data present. If we deserialize the object in the main thread, and change a bit the lua code, we get :
Our object does have the user data pointer to show-off !
An important note for this to work well is that both environments should define the user data type of the object. This way, the receiving environment will be able to use the object given.
Beware that when forwarding the ownership, any environment deserializing data where ownership should be forwarded will take up the ownership flag.
If you need to pass around data to more than one environment, be sure to only forward to one of them. Else, this will most likely mean a crash as more than one call to the destructor will be issued !
To do so, serialization can be done twice, once forwarding and once without forwarding. Then, the buffers can be dispatched to the environments as required.
Serialization and deserialization are done in 2 steps :
As such, this requires some processing and memory allocations to occur. Global tip is to keep the data that has to be serialized as small as possible. This way, the process can be quick, as the messages exchanged are contained.
Serialized buffers can be kept around on disk, and restored when required. As such it becomes possible to do a persistent load and save mechanism.
However, this has one limit : currently, user data cannot be persisted. This is because the only relevant data stored is the C++ pointer. When the program shuts down, this memory pointer has no meaning anymore.
If you need to save states, be sure to rule user data out !
In this tutorial, we learned how serialization / deserialization works and what it has been made for.
We also learned about the ownership of user data and how to transfer it, if required.
With these new tools, it is possible to work with multiple environments, store and restore data, keep different environments in isolation... The only limit is what you cannot imagine !
As a final note, everything that has been described here is the basis for the wrapping of nkTasks within nkAstraeus. It is mentioned the system has its chance to work, but more than that : it works ! And if you need usable multi threading within a scripting environment, consider checking nkAstraeus out :3